package com.lipaluma.csv.io;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.io.IOUtils;

import com.lipaluma.exceptions.CsvParserException;
import com.lipaluma.io.Row;
import com.lipaluma.io.SourceReader;
/**
 * Reads a CSV file
 * 
 * 
 * @author Mario<br/>
 * <br/>
 * <b>inspired by :</b><br/>
 * @author Kasper B. Graversen from super-csv : class Tokenizer
 * @author James Bassett from super-csv : class Tokenizer
 */
public class CsvReader implements SourceReader {
	private static final char NEWLINE = '\n';	
	private static final char SPACE = ' ';
	
	private LineNumberReader scanner;
	private char delimiter;
	private boolean trimValues;
	private Character quoteChar;
	private int currentLineNumber;
	
	/**
	 * create a new CsvReader by InputStream
	 * @param in inputStream of the entry file
	 * @param delimiter the delimiter
	 * @param quoteChar the optional quote
	 * @param trimValues trim all values ?
	 */
	public CsvReader(InputStream in, Character delimiter, Character quoteChar, boolean trimValues) {
		this.scanner = new LineNumberReader(new InputStreamReader(in));
		this.delimiter = delimiter;
		this.quoteChar = quoteChar;
		this.trimValues = trimValues;
	}
	/**
	 * create a new CsvReader by Reader
	 * @param reader reader of the entry file
	 * @param delimiter the delimiter
	 * @param quoteChar the optional quote
	 * @param trimValues trim all values ?
	 */
	public CsvReader(Reader reader, Character delimiter, Character quoteChar, boolean trimValues) {
		this.scanner = new LineNumberReader(reader);
		this.delimiter = delimiter;
		this.quoteChar = quoteChar;
		this.trimValues = trimValues;
	}
	
	public int getCurrentLineNumber() {
		return currentLineNumber;
	}
	
	public boolean hasNext() {
		try {
			char[] buffer = new char[1];
			int read;
			do {
				scanner.mark(1);
				read = scanner.read(buffer);
			} while(read != -1 && NEWLINE == buffer[0]);
			scanner.reset();
			return read != -1;
		} catch (IOException e) {
			throw new CsvParserException("Error on searching existence of next line on the CSV file", e);
		}
	}
	
	public Row next()  {
		try {
			this.currentLineNumber = scanner.getLineNumber();
			String[] line = parseNextLine();
			return new CsvReadRow(line);
		} catch (IOException e) {
			throw new CsvParserException("Error on getting the next line of the CSV file", e);
		}
	}
	
	
	private String[] parseNextLine() throws IOException {
		
		List<String> lineValues = new ArrayList<String>();
		// clear the reusable List and StringBuilders
		StringBuilder currentColumn = new StringBuilder();
		currentColumn.setLength(0);
		
		// keep reading lines until data is found
		String line;
		do {
			line = scanner.readLine();
			if( line == null ) {
				return null; // EOF
			}
		}
		while( "".equals(line) );
		
		// add a newline to determine end of line (making parsing easier)
		line += NEWLINE;
		
		// process each character in the line, catering for surrounding quotes (QUOTE_MODE)
		TokenizerState state = TokenizerState.NORMAL;
		int quoteScopeStartingLine = -1; // the line number where a potential multi-line cell starts
		int potentialSpaces = 0; // keep track of spaces (so leading/trailing space can be removed if required)
		int charIndex = 0;
		while( true ) {
			
			final char c = line.charAt(charIndex);
			
			if( TokenizerState.NORMAL.equals(state) ) {
				
				/*
				 * NORMAL mode (not within quotes).
				 */
				
				if( c == delimiter ) {
					/*
					 * Delimiter. Save the column (trim trailing space if required) then continue to next character.
					 */
					if( !trimValues ) {
						appendSpaces(currentColumn, potentialSpaces);
					}
					lineValues.add(currentColumn.length() > 0 ? currentColumn.toString() : null); // "" -> null
					potentialSpaces = 0;
					currentColumn.setLength(0);
					
				} else if( c == SPACE ) {
					/*
					 * Space. Remember it, then continue to next character.
					 */
					potentialSpaces++;
					
				} else if( c == NEWLINE ) {
					/*
					 * Newline. Add any required spaces (if surrounding spaces don't need quotes) and return (we've read
					 * a line!).
					 */
					if( !trimValues ) {
						appendSpaces(currentColumn, potentialSpaces);
					}
					lineValues.add(currentColumn.length() > 0 ? currentColumn.toString() : null); // "" -> null
					return lineValues.toArray(new String[0]);
					
				} else if( c == quoteChar ) {
					/*
					 * A single quote ("). Update to QUOTESCOPE (but don't save quote), then continue to next character.
					 */
					state = TokenizerState.QUOTE_MODE;
					quoteScopeStartingLine = scanner.getLineNumber();
					
					// cater for spaces before a quoted section (be lenient!)
					if( !trimValues || currentColumn.length() > 0 ) {
						appendSpaces(currentColumn, potentialSpaces);
					}
					potentialSpaces = 0;
					
				} else {
					/*
					 * Just a normal character. Add any required spaces (but trim any leading spaces if surrounding
					 * spaces need quotes), add the character, then continue to next character.
					 */
					if( !trimValues || currentColumn.length() > 0 ) {
						appendSpaces(currentColumn, potentialSpaces);
					}
					
					potentialSpaces = 0;
					currentColumn.append(c);
				}
				
			} else {
				
				/*
				 * QUOTE_MODE (within quotes).
				 */
				
				if( c == NEWLINE ) {
					
					/*
					 * Newline. Doesn't count as newline while in QUOTESCOPE. Add the newline char, reset the charIndex
					 * (will update to 0 for next iteration), read in the next line, then then continue to next
					 * character. For a large file with an unterminated quoted section (no trailing quote), this could
					 * cause memory issues as it will keep reading lines looking for the trailing quote. Maybe there
					 * should be a configurable limit on max lines to read in quoted mode?
					 */
					currentColumn.append(NEWLINE);
					
					charIndex = -1;
					line = scanner.readLine();
					if( line == null ) {
						throw new CsvParserException(
							String
								.format(
									"unexpected end of file while reading quoted column beginning on line %d and ending on line %d",
									quoteScopeStartingLine, scanner.getLineNumber()));
					}
					
					line += NEWLINE; // add newline to simplify parsing
					
				} else if( c == quoteChar ) {
					
					if( line.charAt(charIndex + 1) == quoteChar ) {
						/*
						 * An escaped quote (""). Add a single quote, then move the cursor so the next iteration of the
						 * loop will read the character following the escaped quote.
						 */
						currentColumn.append(c);
						charIndex++;
						
					} else {
						/*
						 * A single quote ("). Update to NORMAL (but don't save quote), then continue to next character.
						 */
						state = TokenizerState.NORMAL;
						quoteScopeStartingLine = -1; // reset ready for next multi-line cell
					}
				} else {
					/*
					 * Just a normal character, delimiter (they don't count in QUOTESCOPE) or space. Add the character,
					 * then continue to next character.
					 */
					currentColumn.append(c);
				}
			}
			
			charIndex++; // read next char of the line
		}
	}

	public void close() {
		IOUtils.closeQuietly(scanner);
	}
	
	/**
	 * Appends the required number of spaces to the StringBuilder.
	 * 
	 * @param sb
	 *            the StringBuilder
	 * @param spaces
	 *            the required number of spaces to append
	 */
	private static void appendSpaces(final StringBuilder sb, final int spaces) {
		for( int i = 0; i < spaces; i++ ) {
			sb.append(SPACE);
		}
	}

	
	private enum TokenizerState {
		NORMAL, QUOTE_MODE;
	}


	public void setDelimiter(char delimiter) {
		this.delimiter = delimiter;
	}
	public void setQuoteChar(char quoteChar) {
		this.quoteChar = quoteChar;
	}
}
